一、题目分析

image-20220411223719263

进入题目后,首页有四个链接:

  • view source code 查看源码
  • go to e-shop 用points 1:1 购买diamonds,diamonds需要达到5
  • reset 重置points
  • 最后一个就是跳转主页面

二、代码审计

很长,看了挺久的

from flask import Flask, session, request, Response
import urllib

app = Flask(__name__)
app.secret_key = '*********************'  # censored
url_prefix = '/d5afe1f66147e857'


def FLAG():
    return '*********************'  # censored


def trigger_event(event):
    session['log'].append(event)  #添加url问号后的参数
    if len(session['log']) > 5:
        session['log'] = session['log'][-5:]  #长度>5 ,则取倒数5位数
    if type(event) == type([]):  #字符串,进入else
        request.event_queue += event
    else:
        request.event_queue.append(event) #数组中添加url问号后的参数


def get_mid_str(haystack, prefix, postfix=None):
    haystack = haystack[haystack.find(prefix)+len(prefix):]
    if postfix is not None:
        haystack = haystack[:haystack.find(postfix)]
    return haystack


class RollBackException:
    pass


def execute_event_loop():
    valid_event_chars = set(
        'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789:;#') #一个无序不重复的元素集,白名单
    resp = None
    while len(request.event_queue) > 0: #event_queue 是一个数组,其中有url问号后的参数
        #对数组所有的元素进行遍历,并处理
        # `event` is something like "action:ACTION;ARGS0#ARGS1#ARGS2......"
        event = request.event_queue[0]  #获取url问号后参数
        request.event_queue = request.event_queue[1:]  #为空
        if not event.startswith(('action:', 'func:')):  #不是以这两个为首的,就要进行白名单处理
            continue
        for c in event:
            if c not in valid_event_chars:
                break
        else:
            is_action = event[0] == 'a' #true or false 首字母为a
            action = get_mid_str(event, ':', ';')  #返回中间参数,view
            args = get_mid_str(event, action+';').split('#')  #返回数组,值为;后面的index
            try:
                event_handler = eval(
                    action + ('_handler' if is_action else '_function'))
                ret_val = event_handler(args)
            except RollBackException:
                if resp is None:
                    resp = ''
                resp += 'ERROR! All transactions have been cancelled. <br />'
                resp += '<a href="./?action:view;index">Go back to index.html</a><br />'
                session['num_items'] = request.prev_session['num_items']
                session['points'] = request.prev_session['points']
                break
            except Exception, e:
                if resp is None:
                    resp = ''
                # resp += str(e) # only for debugging
                continue
            if ret_val is not None:
                if resp is None:
                    resp = ret_val
                else:
                    resp += ret_val
    if resp is None or resp == '':
        resp = ('404 NOT FOUND', 404)
    session.modified = True
    return resp


@app.route(url_prefix+'/')
def entry_point():
    querystring = urllib.unquote(request.query_string) #获取url问号后的参数
    request.event_queue = []
    if querystring == '' or (not querystring.startswith('action:')) or len(querystring) > 100:
        querystring = 'action:index;False#False'
    if 'num_items' not in session:
        session['num_items'] = 0
        session['points'] = 3
        session['log'] = []  #永远是数组最后五个元素
    request.prev_session = dict(session) #字典
    trigger_event(querystring)
    return execute_event_loop()

# handlers/functions below --------------------------------------


def view_handler(args):
    page = args[0]
    html = ''
    html += '[INFO] you have {} diamonds, {} points now.<br />'.format(
        session['num_items'], session['points'])
    if page == 'index':
        html += '<a href="./?action:index;True%23False">View source code</a><br />'
        html += '<a href="./?action:view;shop">Go to e-shop</a><br />'
        html += '<a href="./?action:view;reset">Reset</a><br />'
    elif page == 'shop':
        html += '<a href="./?action:buy;1">Buy a diamond (1 point)</a><br />'
    elif page == 'reset':
        del session['num_items']
        html += 'Session reset.<br />'
    html += '<a href="./?action:view;index">Go back to index.html</a><br />'
    return html


def index_handler(args):
    bool_show_source = str(args[0])
    bool_download_source = str(args[1])
    if bool_show_source == 'True':

        source = open('eventLoop.py', 'r')
        html = ''
        if bool_download_source != 'True':
            html += '<a href="./?action:index;True%23True">Download this .py file</a><br />'
            html += '<a href="./?action:view;index">Go back to index.html</a><br />'

        for line in source:
            if bool_download_source != 'True':
                html += line.replace('&', '&amp;').replace('\t', '&nbsp;'*4).replace(
                    ' ', '&nbsp;').replace('<', '&lt;').replace('>', '&gt;').replace('\n', '<br />')
            else:
                html += line
        source.close()

        if bool_download_source == 'True':
            headers = {}
            headers['Content-Type'] = 'text/plain'
            headers['Content-Disposition'] = 'attachment; filename=serve.py'
            return Response(html, headers=headers)
        else:
            return html
    else:
        trigger_event('action:view;index')


def buy_handler(args):
    num_items = int(args[0])
    if num_items <= 0:
        return 'invalid number({}) of diamonds to buy<br />'.format(args[0])
    session['num_items'] += num_items
    trigger_event(['func:consume_point;{}'.format(
        num_items), 'action:view;index'])


def consume_point_function(args):
    point_to_consume = int(args[0])
    if session['points'] < point_to_consume:
        raise RollBackException()
    session['points'] -= point_to_consume


def show_flag_function(args):
    flag = args[0]
    # return flag # GOTCHA! We noticed that here is a backdoor planted by a hacker which will print the flag, so we disabled it.
    return 'You naughty boy! ;) <br />'


def get_flag_handler(args):
    if session['num_items'] >= 5:
        # show_flag_function has been disabled, no worries
        trigger_event('func:show_flag;' + FLAG())
    trigger_event('action:view;index')


if __name__ == '__main__':
    app.run(debug=False, host='0.0.0.0')

(1)首先看主页路由(程序由此进入):

@app.route(url_prefix+'/')
def entry_point():
    querystring = urllib.unquote(request.query_string) #获取url问号后的参数
    request.event_queue = []
    if querystring == '' or (not querystring.startswith('action:')) or len(querystring) > 100:
        querystring = 'action:index;False#False'
    if 'num_items' not in session:
        session['num_items'] = 0
        session['points'] = 3
        session['log'] = []  #永远是数组最后五个元素
    request.prev_session = dict(session) #字典
    trigger_event(querystring)
    return execute_event_loop()

基本都是一些初始化操作:

  • 初始化 request.event_queue
  • 初始化session
  • 调用函数 trigger_event 处理 querystring(也就是url问号后面的部分)
  • 最后返回 execute_event_loop() (对request.event_queue的处理)

(2)跟进 trigger_event 函数

def trigger_event(event):
    session['log'].append(event)  #添加url问号后的参数
    if len(session['log']) > 5:
        session['log'] = session['log'][-5:]  #长度>5 ,则取倒数5位数
    if type(event) == type([]):  #字符串,进入else
        request.event_queue += event
    else:
        request.event_queue.append(event) #数组中添加url问号后的参数
  • 将我们传入的参数放进了session,session[‘log’] 最大长度为5
  • 将event(最开始的querystring)放入 request.event_queue

(3)跟进 execute_event_loop() 函数

def execute_event_loop():
    valid_event_chars = set(
        'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789:;#') #一个无序不重复的元素集,白名单
    resp = None
    while len(request.event_queue) > 0: #event_queue 是一个数组,其中有url问号后的参数
        #对数组所有的元素进行遍历,并处理
        # `event` is something like "action:ACTION;ARGS0#ARGS1#ARGS2......"
        event = request.event_queue[0]  #获取url问号后参数
        request.event_queue = request.event_queue[1:]  #为空
        if not event.startswith(('action:', 'func:')):  #不是以这两个为首的,就要进行白名单处理
            continue
        for c in event:
            if c not in valid_event_chars:
                break
        else:
            is_action = event[0] == 'a' #true or false 首字母为a
            action = get_mid_str(event, ':', ';')  #返回中间参数,view
            args = get_mid_str(event, action+';').split('#')  #返回数组,值为;后面的index
            try:
                event_handler = eval(
                    action + ('_handler' if is_action else '_function'))
                ret_val = event_handler(args)
            except RollBackException:
                ......
                break
            except Exception, e:
                ......
                continue
            if ret_val is not None:
                if resp is None:
                    resp = ret_val
                else:
                    resp += ret_val
    if resp is None or resp == '':
        resp = ('404 NOT FOUND', 404)
    session.modified = True
    return resp

整体是一个while循环,对数组 request.event_queue 中的元素进行遍历并处理,最后变成一个空数组:

event = request.event_queue[0]  #获取url问号后参数
request.event_queue = request.event_queue[1:]  #为空
  • action是:;之间的值
  • args 是一个数组,;后面,并且用#分隔

接下来就是一个eval函数:(这里很重要)

try:
    event_handler = eval(
    	action + ('_handler' if is_action else '_function'))
    ret_val = event_handler(args)

大概作用就是拼接一个函数名,然后动态调用这个函数,参数为args数组

(4)大致思路

也就是说,对于数组request.event_queue中的值,我们依次去调用其中的函数

request.event_queue的元素是trigger_event函数添加的

剩下就是两个shop功能的函数和一个获取flag的函数

“其实到这,我觉得没有啥问题,所以还是看了WP”

(5)解题思路(这里还是比较难想到的)

这里的重点在于eval函数和#之间的作用上,#在python中是注释符

若是action的最后加上#,我们就可以实现任意函数调用了

接下来就是对于shop购买的处理上的问题:

def buy_handler(args):
    num_items = int(args[0])
    if num_items <= 0:
        return 'invalid number({}) of diamonds to buy<br />'.format(args[0])
    session['num_items'] += num_items
    trigger_event(['func:consume_point;{}'.format(  #增加了diamonds后,再去处理points
        num_items), 'action:view;index'])


def consume_point_function(args):
    point_to_consume = int(args[0])
    if session['points'] < point_to_consume:
        raise RollBackException()
    session['points'] -= point_to_consume

这里的缺陷就是先增加了diamonds,再去减少points,两者直接关联的就是用trigger_event函数处理的数组request.event_queue

而我们可以调用任意函数,其中的参数是可控的

“那我们是不是可以直接调用trigger_event,主动添加要处理的函数,从而达到目的”

(6)解题过程

调用我们想要的函数,在action参数后添加#就行了

Playload:

?action:trigger_event#;action:buy;6#action:get_flag;

url编码:

?action:trigger_event%23;action:buy;6%23action:get_flag;

运行之后,flag就被写入session了:

image-20220411232805828

然后使用工具解析session(flask-session-cookie-manager-master):

image-20220411232858303

依次进行base64解码,找到flag:

image-20220411232930340

三、总结

我认为,做这种大量代码的题目时,首先需要看懂代码各个部分的功能并大概知道其中的操作,最后处理各个部分之间的关系,寻找是否有缺陷,当然最重要的就是对一些细节(函数)的敏感程度。

还是需要多加练习,寻找自己的解题思路。